<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_34</span></h2>

  <p>, 0);</p>

  <p>}</p>

  <p>}</p>

  <p>由于MessageBeep()和MessageBox()函数已在不同的类里被声明成static函数，所以必须在调用它们时规定作用域。大家也许认为必须用上述的方法将所有Win32 API（函数、常数和数据类型）都映射成Java类。但幸运的是，根本不必这样做。</p>

  <p>A.3.2 com.ms.win32包</p>

  <p>Win32 API的体积相当庞大――包含了数以千计的函数、常数以及数据类型。当然，我们并不想将每个Win32 API函数都写成对应Java形式。微软考虑到了这个问题，发行了一个Java包，可通过J/Direct将Win32 API映射成Java类。这个包的名字叫作com.ms.win32。安装Java SDK 2.0时，若在安装选项中进行了相应的设置，这个包就会安装到我们的类路径中。这个包由大量Java类构成，它们完整再现了Win32 API的常数、数据类型以及函数。包容能力最大的三个类是User32.class，Kernel.class以及Gdi32.class。它们包含的是Win32 API的核心内容。为使用它们，只需在自己的Java代码里导入即可。前面的ShowMsgBox示例可用com.ms.win32改写成下面这个样子（这里也考虑到了用更恰当的方式使用UnsatisfiedLinkError）：</p>

  <p>import com.ms.win32.*;</p>

  <p>public class UseWin32Package {</p>

  <p>public static void main(String args[]) {</p>

  <p>try {</p>

  <p>User32.MessageBeep(</p>

  <p>winm.MB_ICONEXCLAMATION);</p>

  <p>User32.MessageBox(0,</p>

  <p>"Created by the MessageBox() Win32 func",</p>

  <p>"Thinking in Java",</p>

  <p>winm.MB_OKCANCEL |</p>

  <p>winm.MB_ICONEXCLAMATION);</p>

  <p>} catch(UnsatisfiedLinkError e) {</p>

  <p>System.out.println("Can’t link Win32 API");</p>

  <p>System.out.println(e);</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>Java包是在第一行导入的。现在，可在不进行其他声明的前提下调用MessageBeep()和MessageBox()函数。在MessageBeep()里，我们可看到包导入时也声明了Win32常数。这些常数是在大量Java接口里定义的，全部命名为winx（x代表欲使用之常数的首字母）。</p>

  <p>写作本书时，com.ms.win32包的开发仍未正式完成，但已可堪使用。</p>

  <p>A.3.3 汇集</p>

  <p>“汇集”（Marshaling）是指将一个函数自变量从它原始的二进制形式转换成与语言无关的某种形式，再将这种通用形式转换成适合调用函数采用的二进制格式。在前面的例子中，我们调用了MessageBox()函数，并向它传递了两个字串。MessageBox()是个C函数，而且Java字串的二进制布局与C字串并不相同。但尽管如此，自变量仍获得了正确的传递。这是由于在调用C代码前，J/Direct已帮我们考虑到了将Java字串转换成C字串的问题。这种情况适合所有标准的Java类型。下面这张表格总结了简单数据类型的默认对应关系：</p>

  <p>Java C</p>

  <p>byte BYTE或CHAR</p>

  <p>short SHORT或WORD</p>

  <p>int INT，UINT，LONG，ULONG或DWORD</p>

  <p>char TCHAR</p>

  <p>long __int64</p>

  <p>float Float</p>

  <p>double Double</p>

  <p>boolean BOOL</p>

  <p>String LPCTSTR（只允许在OLE模式中作为返回值）</p>

  <p>byte[] BYTE *</p>

  <p>short[] WORD *</p>

  <p>char[] TCHAR *</p>

  <p>int[] DWORD *</p>

  <p>这个列表还可继续下去，但已很能说明问题了。大多数情况下，我们不必关心与简单数据类型之间的转换问题。但一旦必须传递用户自定义类型的自变量，情况就立即变得不同了。例如，可能需要传递一个结构化的、用户自定义的数据类型，或者需要把一个指针传给原始内存区域。在这些情况下，有一些特殊的编译引导命令标记一个Java类，使其能作为一个指针传给结构（@dll.struct引导命令）。欲知使用这些关键字的细节，请参考产品文档。</p>

  <p>A.3.4 编写回调函数</p>

  <p>有些Win32 API函数要求将一个函数指针作为自己的参数使用。Windows API函数随后就可以调用自变量函数（通常是在以后发生特定的事件时）。这一技术就叫作“回调函数”。回调函数的例子包括窗口进程以及我们在打印过程中设置的回调（为后台打印程序提供回调函数的地址，使其能更新状态，并在必要的时候中止打印）。</p>

  <p>另一个例子是API函数EnumWindows()，它能枚举目前系统内所有顶级窗口。EnumWindows()要求获取一个函数指针作为自己的参数，然后搜索由Windows内部维护的一个列表。对于列表内的每个窗口，它都会调用回调函数，将窗口句柄作为一个自变量传给回调。</p>

  <p>为了在Java里达到同样的目的，必须使用com.ms.dll包里的Callback类。我们从Callback里继承，并取消callback()。这个方法只能接近int参数，并会返回int或void。方法签名和具体的实施取决于使用这个回调的Windows API函数。</p>

  <p>现在，我们要进行的全部工作就是创建这个Callback衍生类的一个实例，并将其作为函数指针传递给API函数。随后，J/Direct会帮助我们自动完成剩余的工作。</p>

  <p>下面这个例子调用了Win32 API函数EnumWindows()；EnumWindowsProc类里的callback()方法会获取每个顶级窗口的句柄，获取标题文字，并将其打印到控制台窗口。</p>

  <p>import com.ms.dll.*;</p>

  <p>import com.ms.win32.*;</p>

  <p>class EnumWindowsProc extends Callback {</p>

  <p>public boolean callback(int hwnd, int lparam) {</p>

  <p>StringBuffer text = new StringBuffer(50);</p>

  <p>User32.GetWindowText(</p>

  <p>hwnd, text, text.capacity()+1);</p>

  <p>if(text.length() != 0)</p>

  <p>System.out.println(text);</p>

  <p>return true; // to continue enumeration.</p>

  <p>}</p>

  <p>}</p>

  <p>public class ShowCallback {</p>

  <p>public static void main(String args[])</p>

  <p>throws InterruptedException {</p>

  <p>boolean ok = User32.EnumWindows(</p>

  <p>new EnumWindowsProc(), 0);</p>

  <p>if(!ok)</p>

  <p>System.err.println("EnumWindows failed.");</p>

  <p>Thread.currentThread().sleep(3000);</p>

  <p>}</p>

  <p>}</p>

  <p>对sleep()的调用允许窗口进程在main()退出前完成。</p>

  <p>A.3.5 其他J/Direct特性</p>

  <p>通过@dll.import引导命令内的修改符（标记），还可用到J/Direct的另两项特性。第一项是对OLE函数的简化访问，第二项是选择API函数的ANSI及Unicode版本。</p>

  <p>根据约定，所有OLE函数都会返回类型为HRESULT的一个值，它是由COM定义的一个结构化整数值。若在COM那一级编写程序，并希望从一个OLE函数里返回某些不同的东西，就必须将一个特殊的指针传递给它――该指针指向函数即将在其中填充数据的那个内存区域。但在Java中，我们没有指针可用；此外，这种方法并不简练。利用J/Direct，我们可在@dll.import引导命令里使用ole修改符，从而方便地调用OLE函数。标记为ole函数的一个固有方法会从Java形式的方法签名（通过它决定返回类型）自动转换成COM形式的函数。</p>

  <p>第二项特性是选择ANSI或者Unicode字串控制方法。对字串进行控制的大多数Win32 API函数都提供了两个版本。例如，假设我们观察由USER32.DLL导出的符号，那么不会找到一个MessageBox()函数，相反会看到MessageBoxA()和MessageBoxW()函数――分别是该函数的ANSI和Unicode版本。如果在@dll.import引导命令里不规定想调用哪个版本，JVM就会试着自行判断。但这一操作会在程序执行时花费较长的时间。所以，我们一般可用ansi，unicode或auto修改符硬性规定。</p>

  <p>欲了解这些特性更详细的情况，请参考微软公司提供的技术文档。</p>

  <p>A.4 本原接口（RNI）</p>

  <p>同J/Direct相比，RNI是一种比非Java代码复杂得多的接口；但它的功能也十分强大。RNI比J/Direct更接近于JVM，这也使我们能写出更有效的代码，能处理固有方法中的Java对象，而且能实现与JVM内部运行机制更紧密的集成。</p>

  <p>RNI在概念上类似Sun公司的JNI。考虑到这个原因，而且由于该产品尚未正式完工，所以我只在这里指出它们之间的主要差异。欲了解更详细的情况，请参考微软公司的文档。</p>

  <p>JNI和RNI之间存在几方面引人注目的差异。下面列出的是由msjavah生成的C头文件（微软提供的msjavah在功能上相当于Sun的javah），应用于前面在JNI例子里使用的Java类文件ShowMsgBox。</p>

  <p>/* DO NOT EDIT -</p>

  <p>automatically generated by msjavah */</p>

  <p>#include &lt;native.h&gt;</p>

  <p>#pragma warning(disable:4510)</p>

  <p>#pragma warning(disable:4512)</p>

  <p>#pragma warning(disable:4610)</p>

  <p>struct Classjava_lang_String;</p>

  <p>#define Hjava_lang_String Classjava_lang_String</p>

  <p>/* Header for class ShowMsgBox */</p>

  <p>#ifndef _Included_ShowMsgBox</p>

  <p>#define _Included_ShowMsgBox</p>

  <p>#define HShowMsgBox ClassShowMsgBox</p>

  <p>typedef struct ClassShowMsgBox {</p>

  <p>#include &lt;pshpack4.h&gt;</p>

  <p>long MSReserved;</p>

  <p>#include &lt;poppack.h&gt;</p>

  <p>} ClassShowMsgBox;</p>

  <p>#ifdef __cplusplus</p>

  <p>extern "C" {</p>

  <p>#endif</p>

  <p>__declspec(dllexport) void __cdecl</p>

  <p>ShowMsgBox_ShowMessage (struct HShowMsgBox *,</p>

  <p>struct Hjava_lang_String *);</p>

  <p>#ifdef __cplusplus</p>

  <p>}</p>

  <p>#endif</p>

  <p>#endif /* _Included_ShowMsgBox */</p>

  <p>#pragma warning(default:4510)</p>

  <p>#pragma warning(default:4512)</p>

  <p>#pragma warning(default:4610)</p>

  <p>除可读性较差外，代码里还隐藏着一些技术性问题，待我一一道来。</p>

  <p>在RNI中，固有方法的程序员知道对象的二进制布局。这样便允许我们直接访问自己希望的信息；我们不必象在JNI里那样获得一个字段或方法标识符。但由于并非所有虚拟机都需要将相同的二进制布局应用于自己的对象，所以上面的固有方法只能在Microsoft JVM下运行。</p>

  <p>在JNI中，通过JNIEnv自变量可访问大量函数，以便同JVM打交道。在RNI中，用于控制JVM运作的函数变成了可直接调用。它们中的某一些（如控制异常的那一个）类似于它们的JNI“兄弟”。但大多数RNI函数都有与JNI中不同的名字和用途。</p>

  <p>JNI和RNI最重大的一个区别是“垃圾收集”的模型。在JNI中，垃圾收集在固有方法执行期间遵守与Java代码执行时相同的规则。而在RNI中，要由程序员在固有方法活动期间自行负责“垃圾收集器”器的启动与中止。默认情况下，垃圾收集器在进入固有方法前处于不活动状态；这样一来，程序员就可假定准备使用的对象用不着在那个时间段内进行垃圾收集。然而一旦固有方法准备长时间执行，程序员就应考虑激活垃圾收集器――通过调用GCEnable()这个RNI函数（GC是“Garbage Collector”的缩写，即“垃圾收集”）。</p>

  <p>也存在与全局句柄特性类似的机制――程序员可利用可保证特定的对象在GC活动期间不至于被当作“垃圾”收掉。概念是类似的，但名称有所差异――在RNI中，人们把它叫作GCFrames。</p>

  <p>A.4.1 RNI总结</p>

  <p>RNI与Microsoft JVM紧密集成这一事实既是它的优点，也是它的缺点。RNI比JNI复杂得多，但它也为我们提供了对JVM内部活动的高度控制；其中包括垃圾收集。此外，它显然针对速度进行了优化，采纳了C程序员熟悉的一些折衷方案和技术。但除了微软的JVM之外，它并不适于其他JVM。</p>

  <p>A.5 Java/COM集成</p>

  <p>COM（以前称为OLE）代表微软公司的“组件对象模型”（Component Object Model），它是所有ActiveX技术（包括ActiveX控件、Automation以及ActiveX文档）的基础。但COM还包含了更多的东西。它是一种特殊的规范，按照它开发出来的组件对象可通过操作系统的专门特性实现“相互操作”。在实际应用中，为Win32系统开发的所有新软件都与COM有着一定的关系――操作系统通过COM对象揭示出自己的一些特性。由其他厂商开发的组件也可以建立在COM的基础上，我们能创建和注册自己的COM组件。通过这样或那样的形式，如果我们想编写Win32代码，那么必须和COM打交道。在这里，我们将仅仅重述COM编程的基本概念，而且假定读者已掌握了COM服务器（能为COM客户提供服务的任何COM对象）以及COM客户（能从COM服务器那里申请服务的一个COM对象）的概念。本节将尽可能地使叙述变得简单。工具实际的功能要强大得多，而且我们可通过更高级的途径来使用它们。但这也要求对COM有着更深刻的认识，那已经超出了本附录的范围。如果您对这个功能强大、但与不同平台有关的特性感兴趣，应该研究COM和微软公司的文档资料，仔细阅读有关Java/COM集成的那部分内容。如果想获得更多的资料，向您推荐Dale Rogerson编著的《Inside COM》，该书由Microsoft Press于1997年出版。</p>

  <p>由于COM是所有新型Win32应用程序的结构核心，所以通过Java代码使用（或揭示）COM服务的能力就显得尤为重要。Java/COM集成无疑是Microsoft Java编译器以及虚拟机最有趣的特性。Java和COM在它们的模型上是如此相似，所以这个集成在概念上是相当直观的，而且在技术上也能轻松实现无缝结合――为访问COM，几乎不需要编写任何特殊的代码。大多数技术细节都是由编译器和／或虚拟机控制的。最终的结果便是Java程序员可象对待原始Java对象那样对待COM对象。而且COM客户可象使用其他COM服务器那样使用由Java实现的COM服务器。在这里提醒大家，尽管我使用的是通用术语“COM”，但根据扩展，完全可用Java实现一个ActiveX Automation服务器，亦可在Java程序中使用一个ActiveX控件。</p>

  <p>Java和COM最引人注目的相似之处就是COM接口与Java的“interface”关键字的关系。这是接近完美的一种相符，因为：</p>

  <p>■COM对象揭示出了接口（也只有接口）</p>

  <p>■COM接口本身并不具备实施方案；要由揭示出接口的那个COM对象负责它的实施</p>

  <p>■COM接口是对语义上相关的一组函数的说明；不会揭示出任何数据</p>

  <p>■COM类将COM接口组合到了一起。Java类可实现任意数量的Java接口。</p>

  <p>■COM有一个引用对象模型；程序员永远不可能“拥有”一个对象，只能获得对对象一个或多个接口的引用。Java也有一个引用对象模型――对一个对象的引用可“造型”成对它的某个接口的引用。</p>

  <p>■COM对象在内存里的“生存时间”取决于使用对象的客户数量；若这个数量变成零，对象就会将自己从内存中删去。在Java中，一个对象的生存时间也由客户的数量决定。若不再有对那个对象的引用，对象就会等候垃圾收集器的处理。</p>

  <p>Java与COM之间这种紧密的对应关系不仅使Java程序员可以方便地访问COM特性，也使Java成为编写COM代码的一种有效语言。COM是与语言无关的，但COM开发事实上采用的语言是C++和Visual Basic。同Java相比，C++在进行COM开发时显得更加强大，并可生成更有效的代码，只是它很难使用。Visual Basic比Java简单得多，但它距离基础操作系统太远了，而且它的对象模型并未实现与COM很好的对应（映射）关系。Java是两者之间一种很好的折衷方案。</p>

  <p>接下来，让我们对COM开发的一些关键问题进行讨论。编写Java/COM客户和服务器时，这些问题是首先需要弄清楚的。</p>

  <p>A.5.1 COM基础</p>

  <p>COM是一种二进制规范，致力于实施可相互操作的对象。例如，COM认为一个对象的二进制布局必须能够调用另一个COM对象里的服务。由于是对二进制布局的一种描述，所以只要某种语言能生成这样的一种布局，就可通过它实现COM对象。通常，程序员不必关注象这样的一些低级细节，因为编译器可自动生成正确的布局。例如，假设您的程序是用C++写的，那么大多数编译器都能生成符合COM规范的一张虚拟函数表格。对那些不生成可执行代码的语言，比如VB和Java，在运行期则会自动挂接到COM。</p>

  <p>COM库也提供了几个基本的函数，比如用于创建对象或查找系统中一个已注册COM类的函数。</p>

  <p>一个组件对象模型的基本目标包括：</p>

  <p>■让对象调用其他对象里的服务</p>

  <p>■允许新类型对象（或更新对象）无缝插入环境</p>

  <p>第一点正是面向对象程序设计要解决的问题：我们有一个客户对象，它能向一个服务器对象发出请求。在这种情况下，“客户”和“服务器”这两个术语是在常规意义上使用的，并非指一些特定的硬件配置。对于任何面向对象的语言，第一个目标都是很容易达到的――只要您的代码是一个完整的代码块，同时实现了服务器对象代码以及客户对象代码。若改变了客户和服务器对象相互间的沟通形式，只需简单地重新编译和链接一遍即可。重新启动应用程序时，它就会自动采用组件的最新版本。</p>

  <p>但假若应用程序由一些未在自己控制之下的组件对象构成，情况就会变得迥然有异――我们不能控制它们的源码，而且它们的更新可能完全独立于我们的应用程序进行。例如，当我们在自己的程序里使用由其他厂商开发的ActiveX控件时，就会面临这一情况。控件会安装到我们的系统里，我们的程序能够（在运行期）定位服务器代码，激活对象，同它建立链接，然后使用它。以后，我们可安装控件的新版本，我们的应用程序应该仍然能够运行；即使在最糟的情况下，它也应礼貌地报告一条出错消息，比如“控件未找到”等等；一般不会莫名其妙地挂起或死机。</p>

  <p>在这些情况下，我们的组件是在独立的可执行代码文件里实现的：DLL或EXE。若服务器对象在一个独立的可执行代码文件里实现，就需要由操作系统提供的一个标准方法，从而激活这些对象。当然，我们并不想在自己的代码里使用DLL或EXE的物理名称及位置，因为这些参数可能经常发生变化。此时，我们想使用的是由操作系统维护的一些标识符。另外，我们的应用程序需要对服务器展示出来的服务进行的一个描述。下面这两个小节将分别讨论这两个问题。</p>

  <p>1. GUID和注册表</p>

  <p>COM采用结构化的整数值（长度为128位）唯一性地标识系统中注册的COM项目。这些数字的正式名称叫作GUID（Globally Unique IDentifier，全局唯一标识符），可由特殊的工具生成。此外，这些数字可以保证在“任何空间和时间”里独一无二，没有重复。在空间，是由于数字生成器会读取网卡的ID号码；在时间，是由于同时会用到系统的日期和时间。可用GUID标识COM类（此时叫作CLSID）或者COM接口（IID）。尽管名字不同，但基本概念与二进制结构都是相同的。GUID亦可在其他环境中使用，这里不再赘述。</p>

  <p>GUID以及相关的信息都保存在Windows注册表中，或者说保存在“注册数据库”（Registration Database）中。这是一种分级式的数据库，内建于操作系统中，容纳了与系统软硬件配置有关的大量信息。对于COM，注册表会跟踪系统内安装的组件，比如它们的CLSID、实现它们的可执行文件的名字及位置以及其他大量细节。其中一个比较重要的细节是组件的ProgID；ProgID在概念上类似于GUID，因为它们都标识着一个COM组件。区别在于GUID是一个二进制的、通过算法生成的值。而ProgID则是由程序员定义的字串值。ProgID是随同一个CLSID分配的。</p>

  <p>我们说一个COM组件已在系统内注册，最起码的一个条件就是它的CLSID和它的执行文件已存在于注册表中（ProgID通常也已就位）。在后面的例子里，我们主要任务就是注册与使用COM组件。</p>

  <p>注册表的一项重要特点就是它作为客户和服务器对象之间的一个去耦层使用。利用注册表内保存的一些信息，客户会激活服务器；其中一项信息是服务器执行模块的物理位置。若这个位置发生了变动，注册表内的信息就会相应地更新。但这个更新过程对于客户来说是“透明”或者看不见的。后者只需直接使用ProgID或CLSID即可。换句话说，注册表使服务器代码的位置透明成为了可能。随着DCOM（分布式COM）的引入，在本地机器上运行的一个服务器甚至可移到网络中的一台远程机器，整个过程甚至不会引起客户对它的丝毫注意（大多数情况下如此）。</p>

  <p>2. 类型库</p>

  <p>由于COM具有动态链接的能力，同时由于客户和服务器代码可以分开独立发展，所以客户随时都要动态侦测由服务器展示出来的服务。这些服务是用“类型库”（Type Library）中一种二进制的、与语言无关的形式描述的（就象接口和方法签名）。它既可以是一个独立的文件（通常采用.TLB扩展名），也可以是链接到执行程序内部的一种Win32资源。运行期间，客户会利用类型库的信息调用服务器中的函数。</p>

  <p>我们可以写一个Microsoft Interface Definition Language（微软接口定义语言，MIDL）源文件，用MIDL编译器编译它，从而生成一个.TLB文件。MIDL语言的作用是对COM类、接口以及方法进行描述。它在名称、语法以及用途上都类似OMB/CORBA IDL。然而，Java程序员不必使用MIDL。后面还会讲到另一种不同的Microsoft工具，它能读入Java类文件，并能生成一个类型库。</p>

  <p>3. COM:HRESULT中的函数返回代码</p>

  <p>由服务器展示出来的COM函数会返回一个值，采用预先定义好的HRESULT类型。HRESULT代表一个包含了三个字段的整数。这样便可使用多个失败和成功代码，同时还可以使用其他信息。由于COM函数返回的是一个HRESULT，所以不能用返回值从函数调用里取回原始数据。若必须返回数据，可传递指向一个内存区域的指针，函数将在那个区域里填充数据。我们把这称为“外部参数”。作为Java/COM程序员，我们不必过于关注这个问题，因为虚拟机会帮助我们自动照管一切。这个问题将在后续的小节里讲述。</p>

  <p>A.5.2 MS Java/COM集成</p>

  <p>同C++/COM程序员相比，Microsoft Java编译器、虚拟机以及各式各样的工具极大简化了Java/COM程序员的工作。编译器有特殊的引导命令和包，可将Java类当作COM类对待。但在大多数情况下，我们只需依赖Microsoft JVM为COM提供的支持，同时利用两个有力的外部工具。</p>

  <p>Microsoft Java Virtual Machine（JVM）在COM和Java对象之间扮演了一座桥梁的角色。若将Java对象创建成一个COM服务器，那么我们的对象仍然会在JVM内部运行。Microsoft JVM是作为一个DLL实现的，它向操作系统展示出了COM接口。在内部，JVM将对这些COM接口的函数调用映射成Java对象中的方法调用。当然，JVM必须知道哪个Java类文件对应于服务器执行模块；之所以能够找出这方面的信息，是由于我们事前已用Javareg在Windows注册表内注册了类文件。Javareg是与Microsoft Java SDK配套提供的一个工具程序，能读入一个Java类文件，生成相应的类型库以及一个GUID，并可将类注册到系统内。亦可用Javareg注册远程服务器。例如，可用它注册在不同机器上运行的一个服务器。</p>

  <p>如果想写一个Java/COM客户，必须经历一系列不同的步骤。Java/COM“客户”是一些特殊的Java代码，它们想激活和使用系统内注册的一个COM服务器。同样地，虚拟机会与COM服务器沟通，并将它提供的服务作为Java类内的各种方法展示（揭示）出来。另一个Microsoft工具是jactivex，它能读取一个类型库，并生成相应的Java源文件，在其中包含特殊的编译器引导命令。生成的源文件属于我们在指定类型库之后命名的一个包的一部分。下一步是在自己的COM客户Java源文件中导入那个包。</p>

  <p>接下来让我们讨论两个例子。</p>

  <p>A.5.3 用Java设计COM服务器</p>

  <p>本节将介绍ActiveX控件、Automation服务器或者其他任何符合COM规范的服务器的开发过程。下面这个例子实现了一个简单的Automation服务器，它能执行整数加法。我们用setAddend()方法设置addend的值。每次调用sum()方法的时候，addend就会添加到当前result里。我们用getResult()获得result值，并用clear()重新设置值。用于实现这一行为的Java类是非常简单的：</p>

  <p>public class Adder {</p>

  <p>private int addend;</p>

  <p>private int result;</p>

  <p>public void setAddend(int a) { addend = a; }</p>

  <p>public int getAddend() { return addend; }</p>

  <p>public int getResult() { return result; }</p>

  <p>public void sum() { result += addend; }</p>

  <p>public void clear() {</p>

  <p>result = 0;</p>

  <p>addend = 0;</p>

  <p>}</p>

  <p>}</p>

  <p>为了将这个Java类作为一个COM对象使用，我们将Javareg工具应用于编译好的Adder.class文件。这个工具提供了一系列选项；在这种情况下，我们指定Java类文件名（"Adder"），想为这个服务器在注册表里置入的ProgID（"JavaAdder.Adder.1"），以及想为即将生成的类型库指定的名字（"JavaAdder.tlb"）。由于尚未给出CLSID，所以Javareg会自动生成一个。若我们再次对同样的服务器调用Javareg，就会直接使用现成的CLSID。</p>

  <p>javareg /register</p>

  <p>/class:Adder /progid:JavaAdder.Adder.1</p>

  <p>/typelib:JavaAdder.tlb</p>

  <p>Javareg也会将新服务器注册到Windows注册表。此时，我们必须记住将Adder.class复制到Windows\Java\trustlib目录。考虑到安全方面的原因（特别是涉及程序片调用COM服务的问题），只有在COM服务器已安装到trustlib目录的前提下，这些服务器才会被激活。</p>

  <p>现在，我们已在自己的系统中安装了一个新的Automation服务器。为进行测试，我们需要一个Automation控制器，而Automation控制器就是Visual Basic（VB）。在下面，大家会看到几行VB代码。按照VB的格式，我设置了一个文本框，用它从用户那里接收要相加的值。并用一个标签显示结果，用两个下推按钮分别调用sum()和clear()方法。最开始，我们声明了一个名为Adder的对象变量。在Form_Load子例程中（在窗体首次显示时载入），会调用Adder自动服务器的一个新实例，并对窗体的文本字段进行初始化。一旦用户按下“Sum”或者“Clear”按钮，就会调用服务器中对应的方法。</p>

  <p>Dim Adder As Object</p>

  <p>Private Sub Form_Load()</p>

  <p>Set Adder = CreateObject("JavaAdder.Adder.1")</p>

  <p>Addend.Text = Adder.getAddend</p>

  <p>Result.Caption = Adder.getResult</p>

  <p>End Sub</p>

  <p>Private Sub SumBtn_Click()</p>

  <p>Adder.setAddend (Addend.Text)</p>

  <p>Adder.Sum</p>

  <p>Result.Caption = Adder.getResult</p>

  <p>End Sub</p>

  <p>Private Sub ClearBtn_Click()</p>

  <p>Adder.Clear</p>

  <p>Addend.Text = Adder.getAddend</p>

  <p>Result.Caption = Adder.getResult</p>

  <p>End Sub</p>

  <p>注意，这段代码根本不知道服务器是用Java实现的。</p>

  <p>运行这个程序并调用了CreateObject()函数以后，就会在Windows注册表里搜索指定的ProgID。在与ProgID有关的信息中，最重要的是Java类文件的名字。作为一个响应，会启动Java虚拟机，而且在JVM内部调用Java对象的实例。从那个时候开始，JVM就会自动接管客户和服务器代码之间的交流。</p>

  <p>A.5.4 用Java设计COM客户</p>

  <p>现在，让我们转到另一侧，并用Java开发一个COM客户。这个程序会调用系统已安装的COM服务器内的服务。就目前这个例子来说，我们使用的是在前一个例子里为服务器实现的一个客户。尽管代码在Java程序员的眼中看起来比较熟悉，但在幕后发生的一切却并不寻常。本例使用了用Java写成的一个服务器，但它可应用于系统内安装的任何ActiveX控件、ActiveX Automation服务器或者ActiveX组件――只要我们有一个类型库。</p>

  <p>首先，我们将Jactivex工具应用于服务器的类型库。Jactivex有一系列选项和开关可供选择。但它最基本的形式是读取一个类型库，并生成Java源文件。这个源文件保存于我们的windows/java/trustlib目录中。通过下面这行代码，它应用于为外部COM Automation服务器生成的类型库：</p>

  <p>jactivex /javatlb JavaAdder.tlb</p>

  <p>Jactivex完成以后，我们再来看看自己的windows/java/trustlib目录。此时可在其中看到一个新的子目录，名为javaadder。这个目录包含了用于新包的源文件。这是在Java里与类型库的功能差不多的一个库。这些文件需要使用Microsoft编译器的专用引导命令：@com。jactivex生成多个文件的原因是COM使用多个实体来描述一个COM服务器（另一个原因是我没有对MIDL文件和Java/COM工具的使用进行细致的调整）。</p>

  <p>名为Adder.java的文件等价于MIDL文件中的一个coclass引导命令：它是对一个COM类的声明。其他文件则是由服务器揭示出来的COM接口的Java等价物。这些接口（比如Adder_DispatchDefault.java）都属于“遣送”（Dispatch）接口，属于Automation控制器与Automation服务器之间的沟通机制的一部分。Java/COM集成特性也支持双接口的实现与使用。但是，IDispatch和双接口的问题已超出了本附录的范围。</p>

  <p>在下面，大家可看到对应的客户代码。第一行只是导入由jactivex生成的包。然后创建并使用COM Automation服务器的一个实例，就象它是一个原始的Java类那样。请注意行内的类型模型，其中“例示”了COM对象（即生成并调用它的一个实例）。这与COM对象模型是一致的。在COM中，程序员永远不会得到对整个对象的一个引用。相反，他们只能拥有对类内实现的一个或多个接口的引用。</p>

  <p>“例示”Adder类的一个Java对象以后，就相当于指示COM激活服务器，并创建这个COM对象的一个实例。但我们随后必须指定自己想使用哪个接口，在由服务器实现的接口中挑选一个。这正是类型模型完成的工作。这儿使用的是“默认遣送”接口，它是Automation控制器用于同一个Automation服务器通信的标准接口。欲了解这方面的细节，请参考由Ibid编著的《Inside COM》。请注意激活服务器并选择一个COM接口是多么容易！</p>

  <p>import javaadder.*;</p>

  <p>public class JavaClient {</p>

  <p>public static void main(String [] args) {</p>

  <p>Adder_DispatchDefault iAdder =</p>

  <p>(Adder_DispatchDefault) new Adder();</p>

  <p>iAdder.setAddend(3);</p>

  <p>iAdder.sum();</p>

  <p>iAdder.sum();</p>

  <p>iAdder.sum();</p>

  <p>System.out.println(iAdder.getResult());</p>

  <p>}</p>

  <p>}</p>

  <p>现在，我们可以编译它，并开始运行程序。</p>

  <p>1. com.ms.com包</p>

  <p>com.ms.com包为COM的开发定义了数量众多的类。它支持GUID的使用――Variant（变体）和SafeArray Automation（安全数组自动）类型――能与ActiveX控件在一个较深的层次打交道，并可控制COM异常。</p>

  <p>由于篇幅有限，这里不可能涉及所有这些主题。但我想着重强调一下COM异常的问题。根据规范，几乎所有COM函数都会返回一个HRESULT值，它告诉我们函数调用是否成功，以及失败的原因。但若观察服务器和客户代码中的Java方法签名，就会发现没有HRESULT。相反，我们用函数返回值从一些函数那里取回数据。“虚拟机”（VM）会将Java风格的函数调用转换成COM风格的函数调用，甚至包括返回参数。但假若我们在服务器里调用的一个函数在COM这一级失败，又会在虚拟机里出现什么事情呢？在这种情况下，JVM会认为HRESULT值标志着一次失败，并会产生类com.ms.com.ComFailException的一个固有Java异常。这样一来，我们就可用Java异常控制机制来管理COM错误，而不是检查函数的返回值。</p>

  <p>如欲深入了解这个包内包含的类，请参考微软公司的产品文档。</p>

  <p>A.5.5 ActiveX/Beans集成</p>

  <p>Java/COM集成一个有趣的结果就是ActiveX/Beans的集成。也就是说，Java Bean可包含到象VB或任何一种Microsoft Office产品那样的ActiveX容器里。而一个ActiveX控件可包含到象Sun BeanBox这样的Beans容器里。Microsoft JVM会帮助我们考虑到所有的细节。一个ActiveX控件仅仅是一个COM服务器，它展示了预先定义好的、请求的接口。Bean只是一个特殊的Java类，它遵循特定的编程风格。但在写作本书的时候，这一集成仍然不能算作完美。例如，虚拟机不能将JavaBeans事件映射成为COM事件模型。若希望从ActiveX容器内部的一个Bean里对事件加以控制，Bean必须通过低级技术拦截象鼠标行动这类的系统事件，不能采用标准的JavaBeans委托事件模型。</p>

  <p>抛开这个问题不管，ActiveX/Beans集成仍然是非常有趣的。由于牵涉的概念与工具与上面讨论的完全相同，所以请参阅您的Microsoft文档，了解进一步的细节。</p>

  <p>A.5.6 固有方法与程序片的注意事项</p>

  <p>固有方法为我们带来了安全问题的一些考虑。若您的Java代码发出对一个固有方法的调用，就相当于将控制权传递到了虚拟机“体系”的外面。固有方法拥有对操作系统的完全访问权限！当然，如果由自己编写固有方法，这正是我们所希望的。但这对程序片来说却是不可接受的――至少不能默许这样做。我们不想看到从因特网远程服务器下载回来的一个程序片自由自在地操作文件系统以及机器的其他敏感区域，除非特别允许它这样做。为了用J/Direct，RNI和COM集成防止此类情况的发生，只有受到信任（委托）的Java代码才有权发出对固有方法的调用。根据程序片的具体使用，必须满足不同的条件才可放行。例如，使用J/Direct的一个程序片必须拥有数字化签名，指出自己受到完全信任。在写作本书的时候，并不是所有这些安全机制都已实现（对于Microsoft SDK for Java，beta 2版本）。所以当新版本出现以后，请务必留意它的文档说明。</p>

  <p>A.6 CORBA</p>

  <p>在大型的分布式应用中，我们的某些要求并非前面讲述的方法能够满足的。举个例子来说，我们可能想同以前遗留下来的数据仓库打交道，或者需要从一个服务器对象里获取服务，无论它的物理位置在哪里。在这些情况下，都要求某种形式的“远程过程调用”（RPC），而且可能要求与语言无关。此时，CORBA可为我们提供很大的帮助。</p>

  <p>CORBA并非一种语言特性，而是一种集成技术。它代表着一种具体的规范，各个开发商通过遵守这一规范，可设计出符合CORBA标准的集成产品。CORBA规范是由OMG开发出来的。这家非赢利性的机构致力于定义一个标准框架，从而实现分布式、与语言无关对象的相互操作。</p>

  <p>利用CORBA，我们可实现对Java对象以及非Java对象的远程调用，并可与传统的系统进行沟通――采用一种“位置透明”的形式。Java增添了连网支持，是一种优秀的“面向对象”程序设计语言，可构建出图形化和非图形化的应用（程序）。Java和OMG对象模型存在着很好的对应关系；例如，无论Java还是CORBA都实现了“接口”的概念，并且都拥有一个引用（参考）对象模型。</p>

  <p>A.6.1 CORBA基础</p>

  <p>由OMG制订的对象相互操作规范通常称为“对象管理体系”（ObjectManagement Architecture，OMA）。OMA定义了两个组件：“核心对象模型”（Core Object Model）和“OMA参考体系”（OMA Reference Model）。OMA参考体系定义了一套基层服务结构及机制，实现了对象相互间进行操作的能力。OMA参考体系包括“对象请求代理”（Object Request Broker，ORB）、“对象服务”（Object Services，也称作CORBAservices）以及一些通用机制。</p>

  <p>ORB是对象间相互请求的一条通信总线。进行请求时，毋需关心对方的物理位置在哪里。这意味着在客户代码中看起来象一次方案调用的过程实际是非常复杂的一次操作。首先，必须存在与服务器对象的一条连接途径。而且为了创建一个连接，ORB必须知道具体实现服务器的代码存放在哪里。建好连接后，必须对方法自变量进行“汇集”。例如，将它们转换到一个二进制流里，以便通过网络传送。必须传递的其他信息包括服务器的机器名称、服务器进程以及对那个进程内的服务器对象进行标识的信息等等。最后，这些信息通过一种低级线路协议传递，信息在服务器那一端解码，最后正式执行调用。ORB将所有这些复杂的操作都从程序员眼前隐藏起来了，并使程序员的工作几乎和与调用本地对象的方法一样简单。</p>

  <p>并没有硬性规定应如何实现ORB核心，但为了在不同开发商的ORB之间实现一种基本的兼容，OMG定义了一系列服务，它们可通过标准接口访问。</p>

  <p>1. CORBA接口定义语言（IDL）</p>

  <p>CORBA是面向语言的透明而设计的：一个客户对象可调用属于不同类的服务器对象方法，无论对方是用何种语言实现的。当然，客户对象事先必须知道由服务器对象揭示的方法名称及签名。这时便要用到IDL。CORBA IDL是一种与语言无关的设计方法，可用它指定数据类型、属性、操作、接口以及更多的东西。IDL的语法类似于C++或Java语法。下面这张表格为大家总结了三种语言一些通用概念，并展示了它们的对应关系。</p>

  <p>CORBA IDL Java C++</p>

  <p>模块（Module） 包（Package） 命名空间（Namespace）</p>

  <p>接口（Interface） 接口（Interface） 纯抽象类（Pure abstract class）</p>

  <p>方法（Method） 方法（Method） 成员函数（Member function）</p>

  <p>继承概念也获得了支持――就象C++那样，同样使用冒号运算符。针对需要由服务器和客户实现和使用的属性、方法以及接口，程序员要写出一个IDL描述。随后，IDL会由一个由厂商提供的IDL/Java编译器进行编译，后者会读取IDL源码，并生成相应的Java代码。</p>

  <p>IDL编译器是一个相当有用的工具：它不仅生成与IDL等价的Java源码，也会生成用于汇集方法自变量的代码，并可发出远程调用。我们将这种代码称为“根干”（Stub and Skeleton）代码，它组织成多个Java源文件，而且通常属于同一个Java包的一部分。</p>

  <p>2. 命名服务</p>

  <p>命名服务属于CORBA基本服务之一。CORBA对象是通过一个引用访问的。尽管引用信息用我们的眼睛来看没什么意义，但可为引用分配由程序员定义的字串名。这一操作叫作“引用的字串化”。一个叫作“命名服务”（Naming Service）的OMA组件专门用于执行“字串到对象”以及“对象到字串”转换及映射。由于命名服务扮演了服务器和客户都能查询和操作的一个电话本的角色，所以它作为一个独立的进程运行。创建“对象到字串”映射的过程叫作“绑定一个对象”；删除映射关系的过程叫作“取消绑定”；而让对象引用传递一个字串的过程叫作“解析名称”。</p>

  <p>比如在启动的时候，服务器应用可创建一个服务器对象，将对象同命名服务绑定起来，然后等候客户发出请求。客户首先获得一个服务器引用，解析出字串名，然后通过引用发出对服务器的调用。</p>

  <p>同样地，“命名服务”规范也属于CORBA的一部分，但实现它的应用程序是由ORB厂商（开发商）提供的。由于厂商不同，我们访问命名服务的方式也可能有所区别。</p>

  <p>A.6.2 一个例子</p>

  <p>这儿显示的代码可能并不详尽，因为不同的ORB有不同的方法来访问CORBA服务，所以无论什么例子都要取决于具体的厂商（下例使用了JavaIDL，这是Sun公司的一个免费产品。它配套提供了一个简化版本的ORB、一个命名服务以及一个“IDL→Java”编译器）。除此之外，由于Java仍处在发展初期，所以在不同的Java/CORBA产品里并不是包含了所有CORBA特性。</p>

  <p>我们希望实现一个服务器，令其在一些机器上运行，其他机器能向它查询正确的时间。我们也希望实现一个客户，令其请求正确的时间。在这种情况下，我们让两个程序都用Java实现。但在实际应用中，往往分别采用不同的语言。</p>

  <p>1. 编写IDL源码</p>

  <p>第一步是为提供的服务编写一个IDL描述。这通常是由服务器程序员完成的。随后，程序员就可用任何语言实现服务器，只需那种语言里存在着一个CORBA IDL编译器。</p>

  <p>IDL文件已分发给客户端的程序员，并成为两种语言间的桥梁。</p>

  <p>下面这个例子展示了时间服务器的IDL描述情况：</p>

  <p>module RemoteTime {</p>

  <p>interface ExactTime {</p>

  <p>string getTime();</p>

  <p>};</p>

  <p>};</p>

  <p>这是对RemoteTime命名空间内的ExactTime接口的一个声明。该接口由单独一个方法构成，它以字串格式返回当前时间。</p>

  <p>2. 创建根干</p>

  <p>第二步是编译IDL，创建Java根干代码。我们将利用这些代码实现客户和服务器。与JavaIDL产品配套提供的工具是idltojava：</p>

  <p>idltojava -fserver -fclient RemoteTime.idl</p>

  <p>其中两个标记告诉idltojava同时为根和干生成代码。idltojava会生成一个Java包，它在IDL模块、RemoteTime以及生成的Java文件置入RemoteTime子目录后命名。_ExactTimeImplBase.java代表我们用于实现服务器对象的“干”；而_ExactTimeStub.java将用于客户。在ExactTime.java中，用Java方式表示了IDL接口。此外还包含了用到的其他支持文件，例如用于简化访问命名服务的文件。</p>

  <p>3. 实现服务器和客户</p>

  <p>大家在下面看到的是服务器端使用的代码。服务器对象是在ExactTimeServer类里实现的。RemoteTimeServer这个应用的作用是：创建一个服务器对象，通过ORB为其注册，指定对象引用时采用的名称，然后“安静”地等候客户发出请求。</p>

  <p>import RemoteTime.*;</p>

  <p>import org.omg.CosNaming.*;</p>

  <p>import org.omg.CosNaming.NamingContextPackage.*;</p>

  <p>import org.omg.CORBA.*;</p>

  <p>import java.util.*;</p>

  <p>import java.text.*;</p>

  <p>// Server object implementation</p>

  <p>class ExactTimeServer extends _ExactTimeImplBase{</p>

  <p>public String getTime(){</p>

  <p>return DateFormat.</p>

  <p>getTimeInstance(DateFormat.FULL).</p>

  <p>format(new Date(</p>

  <p>System.currentTimeMillis()));</p>

  <p>}</p>

  <p>}</p>

  <p>// Remote application implementation</p>

  <p>public class RemoteTimeServer {</p>

  <p>public static void main(String args[]) {</p>

  <p>try {</p>

  <p>// ORB creation and initialization:</p>

  <p>ORB orb = ORB.init(args, null);</p>

  <p>// Create the server object and register it:</p>

  <p>ExactTimeServer timeServerObjRef =</p>

  <p>new ExactTimeServer();</p>

  <p>orb.connect(timeServerObjRef);</p>

  <p>// Get the root naming context:</p>

  <p>org.omg.CORBA.Object objRef =</p>

  <p>orb.resolve_initial_references(</p>

  <p>"NameService");</p>

  <p>NamingContext ncRef =</p>

  <p>NamingContextHelper.narrow(objRef);</p>

  <p>// Assign a string name to the</p>

  <p>// object reference (binding):</p>

  <p>NameComponent nc =</p>

  <p>new NameComponent("ExactTime", "");</p>

  <p>NameComponent path[] = {nc};</p>

  <p>ncRef.rebind(path, timeServerObjRef);</p>

  <p>// Wait for client requests:</p>

  <p>java.lang.Object sync =</p>

  <p>new java.lang.Object();</p>

  <p>synchronized(sync){</p>

  <p>sync.wait();</p>

  <p>}</p>

  <p>}</p>

  <p>catch (Exception e) {</p>

  <p>System.out.println(</p>

  <p>"Remote Time server error: " + e);</p>

  <p>e.printStackTrace(System.out);</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>正如大家看到的那样，服务器对象的实现是非常简单的；它是一个普通的Java类，从IDL编译器生成的“干”代码中继承而来。但在与ORB以及其他CORBA服务进行联系的时候，情况却变得稍微有些复杂。</p>

  <p>4. 一些CORBA服务</p>

  <p>这里要简单介绍一下JavaIDL相关代码所做的工作（注意暂时忽略了CORBA代码与不同厂商有关这一事实）。main()的第一行代码用于启动ORB。而且理所当然，这正是服务器对象需要同它进行沟通的原因。就在ORB初始化以后，紧接着就创建了一个服务器对象。实际上，它正式名称应该是“短期服务对象”：从客户那里接收请求，“生存时间”与创建它的进程是相同的。创建好短期服务对象后，就会通过ORB对其进行注册。这意味着ORB已知道它的存在，可将请求转发给它。</p>

  <p>到目前为止，我们拥有的全部东西就是一个timeServerObjRef――只有在当前服务器进程里才有效的一个对象引用。下一步是为这个服务对象分配一个字串形式的名字。客户会根据那个名字寻找服务对象。我们通过命名服务（Naming Service）完成这一操作。首先，我们需要对命名服务的一个对象引用。通过调用resolve_initial_references()，可获得对命名服务的字串式对象引用（在JavaIDL中是“NameService”），并将这个引用返回。这是对采用narrow()方法的一个特定NamingContext引用的模型。我们现在可开始使用命名服务了。</p>

  <p>为了将服务对象同一个字串形式的对象引用绑定在一起，我们首先创建一个NameComponent对象，用“ExactTime”进行初始化。“ExactTime”是我们想用于绑定服务对象的名称字串。随后使用rebind()方法，这是受限于对象引用的字串化引用。我们用rebind()分配一个引用――即使它已经存在。而假若引用已经存在，那么bind()会造成一个异常。在CORBA中，名称由一系列NameContext构成――这便是我们为什么要用一个数组将名称与对象引用绑定起来的原因。</p>

  <p>服务对象最好准备好由客户使用。此时，服务器进程会进入一种等候状态。同样地，由于它是一种“短期服务”，所以生存时间要受服务器进程的限制。JavaIDL目前尚未提供对“持久对象”（只要创建它们的进程保持运行状态，对象就会一直存在下去）的支持。</p>

  <p>现在，我们已对服务器代码的工作有了一定的认识。接下来看看客户代码：</p>

  <p>import RemoteTime.*;</p>

  <p>import org.omg.CosNaming.*;</p>

  <p>import org.omg.CORBA.*;</p>

  <p>public class RemoteTimeClient {</p>

  <p>public static void main(String args[]) {</p>

  <p>try {</p>

  <p>// ORB creation and initialization:</p>

  <p>ORB orb = ORB.init(args, null);</p>

  <p>// Get the root naming context:</p>

  <p>org.omg.CORBA.Object objRef =</p>

  <p>orb.resolve_initial_references(</p>

  <p>"NameService");</p>

  <p>NamingContext ncRef =</p>

  <p>NamingContextHelper.narrow(objRef);</p>

  <p>// Get (resolve) the stringified object</p>

  <p>// reference for the time server:</p>

  <p>NameComponent nc =</p>

  <p>new NameComponent("ExactTime", "");</p>

  <p>NameComponent path[] = {nc};</p>

  <p>ExactTime timeObjRef =</p>

  <p>ExactTimeHelper.narrow(</p>

  <p>ncRef.resolve(path));</p>

  <p>// Make requests to the server object:</p>

  <p>String exactTime = timeObjRef.getTime();</p>

  <p>System.out.println(exactTime);</p>

  <p>} catch (Exception e) {</p>

  <p>System.out.println(</p>

  <p>"Remote Time server error: " + e);</p>

  <p>e.printStackTrace(System.out);</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>前几行所做的工作与它们在服务器进程里是一样的：ORB获得初始化，并解析出对命名服务的一个引用。</p>

  <p>接下来，我们需要用到服务对象的一个对象引用，所以将字串形式的对象引用直接传递给resolve()方法，并用narrow()方法将结果造型到ExactTime接口引用里。最后调用getTime()。</p>

  <p>5. 激活名称服务进程</p>

  <p>现在，我们已分别获得了一个服务器和一个客户应用，它们已作好相互间进行沟通的准备。大家知道两者都需要利用命名服务绑定和解析字串形式的对象引用。在运行服务或者客户之前，我们必须启动命名服务进程。在JavaIDL中，命名服务属于一个Java应用，是随产品配套提供的。但它可能与其他产品有所不同。JavaIDL命名服务在JVM的一个实例里运行，并（默认）监视网络端口900。</p>

  <p>6. 激活服务器与客户</p>

  <p>现在，我们已准备好启动服务器和客户应用（之所以按这一顺序，是由于服务器的存在是“短期”的）。若各个方面都设置无误，那么获得的就是在客户控制台窗口内的一行输出文字，提醒我们当前的时间是多少。当然，这一结果本身并没有什么令人兴奋的。但应注意一个问题：即使都处在同一台机器上，客户和服务器应用仍然运行于不同的虚拟机内。它们之间的通信是通过一个基本的集成层进行的――即ORB与命名服务的集成。</p>

  <p>这只是一个简单的例子，面向非网络环境设计。但通常将ORB配置成“与位置无关”。若服务器与客户分别位于不同的机器上，那么ORB可用一个名为“安装库”（Implementation Repository）的组件解析出远程字串式引用。尽管“安装库”属于CORBA的一部分，但它几乎没有具体的规格，所以各厂商的实现方式是不尽相同的。</p>

  <p>正如大家看到的那样，CORBA还有许多方面的问题未在这儿进行详细讲述。但通过以上的介绍，应已对其有一个基本的认识。若想获得CORBA更详细的资料，最传真的起点莫过于OMB Web站点，地址是http://www.omg.org。这个地方提供了丰富的文档资料、白页、程序以及对其他CORBA资源和产品的链接。</p>

  <p>A.6.3 Java程序片和CORBA</p>

  <p>Java程序片可扮演一名CORBA客户的角色。这样一来，程序片就可访问由CORBA对象揭示的远程信息和服务。但程序片只能同最初下载它的那个服务器连接，所以程序片与它沟通的所有CORBA对象都必须位于那台服务器上。这与CORBA的宗旨是相悖的：它许诺可以实现“位置的透明”，或者“与位置无关”。</p>

  <p>将Java程序片作为CORBA客户使用时，也会带来一些安全方面的问题。如果您在内联网中，一个办法是放宽对浏览器的安全限制。或者设置一道防火墙，以便建立与外部服务器安全连接。</p>

  <p>针对这一问题，有些Java ORB产品专门提供了自己的解决方案。例如，有些产品实现了一种名为“HTTP通道”（HTTP Tunneling）的技术，另一些则提供了特别的防火墙功能。</p>

  <p>作为放到附录中的内容，所有这些主题都显得太复杂了。但它们确实是需要重点注意的问题。</p>

  <p>A.6.4 比较CORBA与RMI</p>

  <p>我们已经知道，CORBA的一项主要特性就是对RPC（远程过程调用）的支持。利用这一技术，我们的本地对象可调用位置远程对象内的方法。当然，目前已有一项固有的Java特性可以做完全相同的事情：RMI（参考第15章）。尽管RMI使Java对象之间进行RPC调用成为可能，但CORBA能在用任何语言编制的对象之间进行RPC。这显然是一项很大的区别。</p>

  <p>然而，可通过RMI调用远程、非Java代码的服务。我们需要的全部东西就是位于服务器那一端的、某种形式的封装Java对象，它将非Java代码“包裹”于其中。封装对象通过RMI同Java客户建立外部连接，并于内部建立与非Java代码的连接――采用前面讲到的某种技术，如JNI或J/Direct。</p>

  <p>使用这种方法时，要求我们编写某种类型的“集成层”――这其实正是CORBA帮我们做的事情。但是这样做以后，就不再需要其他厂商开发的ORB了。</p>

  <p>A.7 总结</p>

  <p>我们在这个附录讨论的都是从一个Java应用里调用非Java代码最基本的技术。每种技术都有自己的优缺点。但目前最主要的问题是并非所有这些特性都能在所有JVM中找到。因此，即使一个Java程序能调用位于特定平台上的固有方法，仍有可能不适用于安装了不同JVM的另一种平台。</p>

  <p>Sun公司提供的JNI具有灵活、简单（尽管它要求对JVM内核进行大量控制）、功能强大以及通用于大多数JVM的优点。到本书完稿时为止，微软仍未提供对JNI的支持，而是提供了自己的J/Direct（调用Win32 DLL函数的一种简便方法）和RNI（特别适合编写高效率的代码，但要求对JVM内核有很深入的理解）。微软也提供了自己的专利Java/COM集成方案。这一方案具有很强大的功能，且将Java变成了编写COM服务器和客户的有效语言。只有微软公司的编译器和JVM能提供对J/Direct、RNI以及Java/COM的支持。</p>

  <p>我们最后研究的是CORBA，它使我们的Java对象可与其他对象沟通――无论它们的物理位置在哪里，也无论是用何种语言实现的。CORBA与前面提到的所有技术都不同，因为它并未集成到Java语言里，而是采用了其他厂商（第三方）的集成技术，并要求我们购买其他厂商提供的ORB。CORBA是一种有趣和通用的方案，但如果只是想发出对操作系统的调用，它也许并非一种最佳方案。英文版主页 | 中文版主页 | 详细目录 | 关于译者</p>

  <p>-------------------------------------------------</p>

  <p>TXT书库 www.16txt.com</p>

  <p>-------------------------------------------------附录B 对比C++和Java</p>

  <p>“作为一名C++程序员，我们早已掌握了面向对象程序设计的基本概念，而且Java的语法无疑是非常熟悉的。事实上，Java本来就是从C++衍生出来的。”</p>

  <p>然而，C++和Java之间仍存在一些显著的差异。可以这样说，这些差异代表着技术的极大进步。一旦我们弄清楚了这些差异，就会理解为什么说Java是一种优秀的程序设计语言。本附录将引导大家认识用于区分Java和C++的一些重要特征。</p>

  <p>(1) 最大的障碍在于速度：解释过的Java要比C的执行速度慢上约20倍。无论什么都不能阻止Java语言进行编译。写作本书的时候，刚刚出现了一些准实时编译器，它们能显著加快速度。当然，我们完全有理由认为会出现适用于更多流行平台的纯固有编译器，但假若没有那些编译器，由于速度的限制，必须有些问题是Java不能解决的。</p>

  <p>(2) 和C++一样，Java也提供了两种类型的注释。</p>

  <p>(3) 所有东西都必须置入一个类。不存在全局函数或者全局数据。如果想获得与全局函数等价的功能，可考虑将static方法和static数据置入一个类里。注意没有象结构、枚举或者联合这一类的东西，一切只有“类”（Class）！</p>

  <p>(4) 所有方法都是在类的主体定义的。所以用C++的眼光看，似乎所有函数都已嵌入，但实情并非如何（嵌入的问题在后面讲述）。</p>

  <p>(5) 在Java中，类定义采取几乎和C++一样的形式。但没有标志结束的分号。没有class foo这种形式的类声明，只有类定义。</p>

  <p>class aType()</p>

  <p>void aMethod() {/* 方法主体 */}</p>

  <p>}</p>

  <p>(6) Java中没有作用域范围运算符“::”。Java利用点号做所有的事情，但可以不用考虑它，因为只能在一个类里定义元素。即使那些方法定义，也必须在一个类的内部，所以根本没有必要指定作用域的范围。我们注意到的一项差异是对static方法的调用：使用ClassName.methodName()。除此以外，package（包）的名字是用点号建立的，并能用import关键字实现C++的“#include”的一部分功能。例如下面这个语句：</p>

  <p>import java.awt.*;</p>

  <p>（#include并不直接映射成import，但在使用时有类似的感觉。）</p>

  <p>(7) 与C++类似，Java含有一系列“主类型”（Primitive type），以实现更有效率的访问。在Java中，这些类型包括boolean，char，byte，short，int，long，float以及double。所有主类型的大小都是固有的，且与具体的机器无关（考虑到移植的问题）。这肯定会对性能造成一定的影响，具体取决于不同的机器。对类型的检查和要求在Java里变得更苛刻。例如：</p>

  <p>■条件表达式只能是boolean（布尔）类型，不可使用整数。</p>

  <p>■必须使用象X+Y这样的一个表达式的结果；不能仅仅用“X+Y”来实现“副作用”。</p>

  <p>(8) char（字符）类型使用国际通用的16位Unicode字符集，所以能自动表达大多数国家的字符。</p>

  <p>(9) 静态引用的字串会自动转换成String对象。和C及C++不同，没有独立的静态字符数组字串可供使用。</p>

  <p>(10) Java增添了三个右移位运算符“&gt;&gt;&gt;”，具有与“逻辑”右移位运算符类似的功用，可在最末尾插入零值。“&gt;&gt;”则会在移位的同时插入符号位（即“算术”移位）。</p>

  <p>(11) 尽管表面上类似，但与C++相比，Java数组采用的是一个颇为不同的结构，并具有独特的行为。有一个只读的length成员，通过它可知道数组有多大。而且一旦超过数组边界，运行期检查会自动丢弃一个异常。所有数组都是在内存“堆”里创建的，我们可将一个数组分配给另一个（只是简单地复制数组句柄）。数组标识符属于第一级对象，它的所有方法通常都适用于其他所有对象。</p>

  <p>(12) 对于所有不属于主类型的对象，都只能通过new命令创建。和C++不同，Java没有相应的命令可以“在堆栈上”创建不属于主类型的对象。所有主类型都只能在堆栈上创建，同时不使用new命令。所有主要的类都有自己的“封装（器）”类，所以能够通过new创建等价的、以内存“堆”为基础的对象（主类型数组是一个例外：它们可象C++那样通过集合初始化进行分配，或者使用new）。</p>

  <p>(13) Java中不必进行提前声明。若想在定义前使用一个类或方法，只需直接使用它即可――编译器会保证使用恰当的定义。所以和在C++中不同，我们不会碰到任何涉及提前引用的问题。</p>

  <p>(14) Java没有预处理机。若想使用另一个库里的类，只需使用import命令，并指定库名即可。不存在类似于预处理机的宏。</p>

  <p>(15) Java用包代替了命名空间。由于将所有东西都置入一个类，而且由于采用了一种名为“封装”的机制，它能针对类名进行类似于命名空间分解的操作，所以命名的问题不再进入我们的考虑之列。数据包也会在单独一个库名下收集库的组件。我们只需简单地“import”（导入）一个包，剩下的工作会由编译器自动完成。</p>

  <p>(16) 被定义成类成员的对象句柄会自动初始化成null。对基本类数据成员的初始化在Java里得到了可靠的保障。若不明确地进行初始化，它们就会得到一个默认值（零或等价的值）。可对它们进行明确的初始化（显式初始化）：要么在类内定义它们，要么在构建器中定义。采用的语法比C++的语法更容易理解，而且对于static和非static成员来说都是固定不变的。我们不必从外部定义static成员的存储方式，这和C++是不同的。</p>

  <p>(17) 在Java里，没有象C和C++那样的指针。用new创建一个对象的时候，会获得一个引用（本书一直将其称作“句柄”）。例如：</p>

  <p>String s = new String("howdy");</p>

  <p>然而，C++引用在创建时必须进行初始化，而且不可重定义到一个不同的位置。但Java引用并不一定局限于创建时的位置。它们可根据情况任意定义，这便消除了对指针的部分需求。在C和C++里大量采用指针的另一个原因是为了能指向任意一个内存位置（这同时会使它们变得不安全，也是Java不提供这一支持的原因）。指针通常被看作在基本变量数组中四处移动的一种有效手段。Java允许我们以更安全的形式达到相同的目标。解决指针问题的终极方法是“固有方法”（已在附录A讨论）。将指针传递给方法时，通常不会带来太大的问题，因为此时没有全局函数，只有类。而且我们可传递对对象的引用。Java语言最开始声称自己“完全不采用指针！”但随着许多程序员都质问没有指针如何工作？于是后来又声明“采用受到限制的指针”。大家可自行判断它是否“真”的是一个指针。但不管在何种情况下，都不存在指针“算术”。</p>

  <p>(18) Java提供了与C++类似的“构建器”（Constructor）。如果不自己定义一个，就会获得一个默认构建器。而如果定义了一个非默认的构建器，就不会为我们自动定义默认构建器。这和C++是一样的。注意没有复制构建器，因为所有自变量都是按引用传递的。</p>

  <p>(19) Java中没有“破坏器”（Destructor）。变量不存在“作用域”的问题。一个对象的“存在时间”是由对象的存在时间决定的，并非由垃圾收集器决定。有个finalize()方法是每一个类的成员，它在某种程度上类似于C++的“破坏器”。但finalize()是由垃圾收集器调用的，而且只负责释放“资源”（如打开的文件、套接字、端口、URL等等）。如需在一个特定的地点做某样事情，必须创建一个特殊的方法，并调用它，不能依赖finalize()。而在另一方面，C++中的所有对象都会（或者说“应该”）破坏，但并非Java中的所有对象都会被当作“垃圾”收集掉。由于Java不支持破坏器的概念，所以在必要的时候，必须谨慎地创建一个清除方法。而且针对类内的基础类以及成员对象，需要明确调用所有清除方法。</p>

  <p>(20) Java具有方法“过载”机制，它的工作原理与C++函数的过载几乎是完全相同的。</p>

  <p>(21) Java不支持默认自变量。</p>

  <p>(22) Java中没有goto。它采取的无条件跳转机制是“break 标签”或者“continue 标准”，用于跳出当前的多重嵌套循环。</p>

  <p>(23) Java采用了一种单根式的分级结构，因此所有对象都是从根类Object统一继承的。而在C++中，我们可在任何地方启动一个新的继承树，所以最后往往看到包含了大量树的“一片森林”。在Java中，我们无论如何都只有一个分级结构。尽管这表面上看似乎造成了限制，但由于我们知道每个对象肯定至少有一个Object接口，所以往往能获得更强大的能力。C++目前似乎是唯一没有强制单根结构的唯一一种OO语言。</p>

  <p>(24) Java没有模板或者参数化类型的其他形式。它提供了一系列集合：Vector（向量），Stack（堆栈）以及Hashtable（散列表），用于容纳Object引用。利用这些集合，我们的一系列要求可得到满足。但这些集合并非是为实现象C++“标准模板库”（STL）那样的快速调用而设计的。Java 1.2中的新集合显得更加完整，但仍不具备正宗模板那样的高效率使用手段。</p>

  <p>(25) “垃圾收集”意味着在Java中出现内存漏洞的情况会少得多，但也并非完全不可能（若调用一个用于分配存储空间的固有方法，垃圾收集器就不能对其进行跟踪监视）。然而，内存漏洞和资源漏洞多是由于编写不当的finalize()造成的，或是由于在已分配的一个块尾释放一种资源造成的（“破坏器”在此时显得特别方便）。垃圾收集器是在C++基础上的一种极大进步，使许多编程问题消弥于无形之中。但对少数几个垃圾收集器力有不逮的问题，它却是不大适合的。但垃圾收集器的大量优点也使这一处缺点显得微不足道。</p>

  <p>(26) Java内建了对多线程的支持。利用一个特殊的Thread类，我们可通过继承创建一个新线程（放弃了run()方法）。若将synchronized（同步）关键字作为方法的一个类型限制符使用，相互排斥现象会在对象这一级发生。在任何给定的时间，只有一个线程能使用一个对象的synchronized方法。在另一方面，一个synchronized方法进入以后，它首先会“锁定”对象，防止其他任何synchronized方法再使用那个对象。只有退出了这个方法，才会将对象“解锁”。在线程之间，我们仍然要负责实现更复杂的同步机制，方法是创建自己的“监视器”类。递归的synchronized方法可以正常运作。若线程的优先等级相同，则时间的“分片”不能得到保证。</p>

  <p>(27) 我们不是象C++那样控制声明代码块，而是将访问限定符（public，private和protected）置入每个类成员的定义里。若未规定一个“显式”（明确的）限定符，就会默认为“友好的”（friendly）。这意味着同一个包里的其他元素也可以访问它（相当于它们都成为C++的“friends”――朋友），但不可由包外的任何元素访问。类――以及类内的每个方法――都有一个访问限定符，决定它是否能在文件的外部“可见”。private关键字通常很少在Java中使用，因为与排斥同一个包内其他类的访问相比，“友好的”访问通常更加有用。然而，在多线程的环境中，对private的恰当运用是非常重要的。Java的protected关键字意味着“可由继承者访问，亦可由包内其他元素访问”。注意Java没有与C++的protected关键字等价的元素，后者意味着“只能由继承者访问</p>

  <div class="mbppagebreak"></div>
</body>
</html>
